Ce Challenge de Kaggle qui a pour destination l'identification du nom de baleines déjà identifiées (et "labelisées")
- Dans le cas où celle-ci n'est pas 'connue' elle doit être catégorisée comme étant `new_whale`Le fichier de soumission doit contenir le nom de l'image de test ainsi que 5 prédictions de noms de baleines
Le fait d'ajouter un 2e, 3e ... nom ne pénalise pas plus, il est donc souhaitable de remplir les 5 noms possibles.
Le résultat prend en compte l'ordre de prédiction des noms- le bon nom de classe proposé en 1e position obtiendra 1 - le bon nom est en position 2 : 1/2 - en 3 : 1/3 - en 4 : 1/4 - en 5 : 1/5C'est la moyenne des notes de toutes les images qui est la mesure de performance
dir_data = './../../DATAS/data/'
dir_image_train = dir_data + 'train/'
dir_image_test = dir_data + 'test/'
csv_file = dir_data + 'train.csv'
import pandas as pd
import numpy as np
import os
import matplotlib.pyplot as plt
## Fonctions Utiles
from keras.preprocessing.image import ImageDataGenerator, image
from Utils import TransformImage
df_train = pd.read_csv(csv_file)
df_train.head()
df_train.shape, len(os.listdir(dir_image_train)), len(os.listdir(dir_image_test))
len(set(list(df_train.Image))-set(os.listdir(dir_image_train)))
Nous avons donc 25 361 images dans le répertoire d'entrainement, avec les mêmes noms d'images que celles du fichier 'train.csv'
Nous avons 7 960 images dans le répertoire de test pour générer les fichiers de soumission
df_train.head()
def image_with_index(idx):
return np.array(image.load_img(dir_image_train + df_train.loc[idx].Image))
def image_name_id_from_index(idx):
val = df_train.loc[idx]
return '{} / {}'.format(val['Image'], val['Id'])
def get_image_name_id_from_index(idx):
return df_train.loc[idx]['Image']
def image_with_name(image_name):
return np.array(image.load_img(dir_image_train + image_name))
l_index_image_dataSet = [0, 10, 100, 4000, 12000, 13000, 13001, 13002, 13003]
fig, ax = plt.subplots(3, 3, figsize=(20, 8))
ax = ax.ravel()
for idx_axe, idx_image in enumerate(l_index_image_dataSet):
ax[idx_axe].imshow(image_with_index(idx_image))
ax[idx_axe].axis('off')
ax[idx_axe].set_title(image_name_id_from_index(idx_image))
plt.suptitle("Exemple d'Images du DataSet", fontsize=22)
plt.show()
Regardons des Images appartenant à la même classe
Head = df_train.Id.value_counts().sort_values(ascending=False).head(10)
Tail = df_train.Id.value_counts().sort_values(ascending=False).tail(10)
All = df_train.Id.value_counts().sort_values(ascending=False)
t2 = pd.concat([Head, Tail])
fig, ax = plt.subplots(2, 2, figsize=(20, 13))
fig.subplots_adjust(hspace=0.55)
ax = ax.ravel()
u1 = Head.plot(kind='bar', ax=ax[0], title="Nombre d'Image par classe les plus représentées")
Tail.plot(kind='bar', ax=ax[1], title="Nombre d'Image par classe les moins représentées")
t2.plot(kind='bar', ax=ax[2], title="Nombre d'Image par classe\nMixte entre faible et grosses représentations")
t2[t2.values<2000].plot(kind='bar', ax=ax[3], title="Nombre d'Image par classe\n"
"Mixte entre faible et grosses représentations\nen excluant 'new_whale'")
plt.show()
def affiche_exemple_image_meme_class(CLASS_NAME):
l_index_image_dataSet = list(df_train[df_train.Id==CLASS_NAME].index[:3])
fig, ax = plt.subplots(1, 3, )
fig.set_size_inches(15, 4, forward=True)
ax = ax.ravel()
for idx_axe, idx_image in enumerate(l_index_image_dataSet):
ax[idx_axe].imshow(image_with_index(idx_image))
ax[idx_axe].axis('off')
ax[idx_axe].set_title(image_name_id_from_index(idx_image))
plt.suptitle("Exemple d'Images de la classe {}".format(CLASS_NAME), fontsize=22)
plt.show()
affiche_exemple_image_meme_class(CLASS_NAME = 'w_23a388d')
affiche_exemple_image_meme_class(CLASS_NAME = 'w_9b5109b')
affiche_exemple_image_meme_class(CLASS_NAME = 'w_0369a5c')
affiche_exemple_image_meme_class(CLASS_NAME = 'w_88e4537')
# Nombre d'images pour les classes les plus représentées
Head
print('Nombre de classes différentes : ', df_train.Id.nunique())
on remarque une disparité très importante entre la représentation de 'new_whale' (c'est normal) et les autres
def tag_from_frequence(freq_in):
if freq_in>=1000: return '[ 80, $\infty$ ['
elif freq_in>=50: return '[ 50, 80 ['
elif freq_in>=20: return '[ 20, 50 ['
elif freq_in>=10: return '[ 10, 20 ['
elif freq_in>=6: return '[ 6, 10 ['
elif freq_in>=5: return '[ 5 ]'
elif freq_in>=4: return '[ 4 ]'
elif freq_in>=3: return '[ 3 ]'
elif freq_in>=2: return '[ 2 ]'
else: return '[ 1 ]'
def tag_from_frequence_order(freq_in):
if freq_in>=1000: return 16
elif freq_in>=50: return 15
elif freq_in>=20: return 14
elif freq_in>=10: return 13
elif freq_in>=6: return 12
elif freq_in>=5: return 11
elif freq_in>=4: return 10
elif freq_in>=3: return 9
elif freq_in>=2: return 8
else: return 0
ensembles = [ tag_from_frequence(x) for x in df_train.Id.value_counts().values]
ensembles_ord = [ tag_from_frequence_order(x) for x in df_train.Id.value_counts().values]
t1 = pd.DataFrame({'Ensemble':ensembles, 'Ordre':ensembles_ord})
t2 = t1.groupby(['Ensemble', 'Ordre']).size().reset_index(name='Freq')
t2.sort_values(by=['Ordre'], inplace=True)
fig, ax = plt.subplots()
fig.set_size_inches(15, 7, forward=False)
rect1 = plt.bar(x=t2['Ensemble'], height=t2['Freq'], data=t2['Freq'])
plt.title("Nombre de classe par nombre d'image par classe ", fontsize=22)
plt.xlabel("Nombre d'Images par classe")
plt.ylabel("Nombre de classes")
def autolabel(rects):
"""
Attach a text label above each bar displaying its height
"""
for rect in rects:
height = rect.get_height()
ax.text(rect.get_x() + rect.get_width()/2., 1.05*height,
'%d' % int(height),
ha='center', va='bottom', color='red')
autolabel(rect1)
plt.show()
Une stratégie globale pour traiter cette problématique peut donc être de
Afin de suivre la stratégie globale pour répondre aux besoins du Challenge j'ai défini 4 stratégies qui permettent de mettre de suivre celle-ci.
1. Modéliser la classification (new_whale compris) :
-> Donner les 5 meilleurs score dans l'ordre (ou avec une pondération à définir)
2. Modéliser la classification (new_whale compris) :
-> Etablir une notion de distance à l'appartenance de chaque classe pour ajouter un élément 'new_whale' et sa place à proposer dans la liste des propositions par l'établissement d'un seuil, par clustering, ...
3. Modéliser la classification (en excluant les images labellisées 'new_whale') :
-> Donner les 5 meilleurs score pour la prédiction des images de test, modéliser la détection d'appartenance à 'new_whale' par rapport aux scores des noms proposés.
4. (solution à présenter après -- est-ce new_whale ou autre :
-> Si c'est autre modéliser l'appartenance à tel ou tel groupe)
Pour traiter notre problématique de modélisation de 5005 classes à partir de ces images, l'utilisation du DEEP LEARNING au travers d'un CNN ( Convolutional Neural Network) ou Réseau de Neurones Convolutionnel semble adapté
Nous l'avons vu, la distribution des données mises à disposition est désequilibrée,
Il s'agit donc d'un petit dataSet (faible représentation des classes) pour un grand nombre de classes (5005)
Nous allons utiliser des CNN entrainés sur des images d'ImageNet
Dans le cas de CNN où l'on a une faible représentation de certaines classes un principe est très utile c'est celui de la Data Augmentation qui consiste à ajouter des images synthétiques générées via des transformations aléatoires à des images existantes.
On peut par exemple faire les transformations suivantes :
translations verticales, horizontales
rotations d'images
miroring horizontal et vertical
modification du contraste de l'image
zoom avant ou arrière
réduction de bruit blanc (zca whitening)
Ainsi on augmente son DataSet de données nouvelles qui vont améliorer les performances de l'entrainement du modèle.
# Definition du ImageDataGenerator c'est à dire les paramètres des transformations
train_datagen = ImageDataGenerator(
rescale=1./255,
rotation_range=20,
width_shift_range=0.2,
height_shift_range=0.2,
horizontal_flip=True,
fill_mode='nearest',
brightness_range=[0.40, 0.8],
zoom_range=[0.7, 1.7],
)
MyTransformation = TransformImage(
train_datagen,
resize_max_pixel=300000,
dir_input=dir_image_train,
dir_output=dir_image_train,
prefix_rename='AUG')
def affiche_transformation(l_index_image_dataSet):
fig, ax = plt.subplots(3, 4, )
ax = ax.ravel()
fig.set_size_inches(20, 8, forward=True)
#l_index_image_dataSet = [0, 10, 100, 4000, 12000, 13000, 13001, 13002, 13003]
idx_axe = 0
for idx, idx_image in enumerate(l_index_image_dataSet):
image_name = get_image_name_id_from_index(idx_image)
ax[idx_axe].imshow(image_with_index(idx_image))
ax[idx_axe].axis('off')
ax[idx_axe].set_title('{} originale'.format(image_name))
idx_axe+=1
for i in range(3):
ax[idx_axe].imshow(np.array(MyTransformation.transform(filename=image_name)))
ax[idx_axe].axis('off')
idx_axe+=1
plt.suptitle('Transformations aléatoires définie par le train_datagen', fontsize=22)
plt.show()
affiche_transformation([0, 10, 100, ])
affiche_transformation([4000, 12000, 13000])
Nous avons aussi remarqué que de très nombreuses classes n'ont qu'une image, ou seulement 2 ou 3, ce qui est très peu.
Un problème qui se pose notamment (en plus de la difficulté à optimisser la performance) est qu'il n'est même pas possible de réaliser les différentes phases que sont l'entrainement / la validation et optimisation et le test de performance du modèle avec si peu de données.
def genere_list_name_from_a_class(name, df, nb_to_create=5):
'''Génération d'un DataFrame avec le nom des image à dupliquer'''
l1 = list(df[df.Id==name].Image.values)*nb_to_create
# On laisse jouer le random pour permuter les listes
shuffle(l1)
df_tmp = pd.DataFrame({'Image': l1[:nb_to_create]})
df_tmp['Id'] = name
return df_tmp
def create_df_with_image_to_augment(df_in, min_occurence=6):
# On identifie l'occurrence de chaque classe
frequences = df_in.Id.value_counts()
# On ne traite que celles manquant d'images
df_to_treat = frequences[frequences.values<min_occurence]
l_df_with_names = []
# On lance la génération d'image classe par classe
for name, nb_occ in zip(df_to_treat.index, df_to_treat.values):
tmp_df = genere_list_name_from_a_class(
name,
df_in,
nb_to_create=min_occurence-nb_occ)
l_df_with_names.append(tmp_df)
# On renvoie le dataframe généré
return pd.concat(l_df_with_names).reset_index(drop=True)
def genere_image_aug(df_in, myImageDataGenerator, min_occurence=6):
'''Fonction qui
- recupere la liste des images a dupliquer
- genere les nouvelles images
- renvoie un dataframe avec les nouveau nom et les Id
'''
# Liste des images à dupliquer
new_dataframe = create_df_with_image_to_augment(df_in, min_occurence=6)
# Les Paths
path_in='./../DATAS/data/train'
path_out = './../DATAS/data/aug'
# On Initialise la classe de tranformation des Images
MyTransformation = TransformImage(
train_datagen,
resize_max_pixel=300000,
dir_input=path_in,
dir_output=path_out,
prefix_rename='AUG')
idx_nb_traite = 0
l_new_name = []
# On lance la transformation des Images
for m_name, m_class in zip(new_dataframe.Image, new_dataframe.Id):
idx_nb_traite+=1
# Transformation aléatoire de l'image et sauvegarde
new_name = MyTransformation.transform_and_save(filename=m_name)
l_new_name.append(new_name)
return pd.DataFrame({'Image': l_new_name, 'Id': new_dataframe.Id})
## Execution de la génération des Images
df_image_aug = genere_image_aug(df_train, train_datagen, min_occurence=6)
Comme nous l'avons dit nous utilisons un réseau convolutionel de neurones pour faire la classification de nos images.
Nous utilisons ici une solution de Transfer Learning à partir du VGG16 pré-entrainé sur les images d'ImageNet (base d'Images initiée par l'équipe de Li Fei-Fei de l'Université de Stanford).

Des tests ont aussi été réalisés via InceptionV3.
Nous séparons les données de train en 3 parties : train (60%) / validation (20%) / split (20%
à l'aide de la fonction train_test_split par stratification, ce qui permet de ne pas avoir de classes non vues dans le train ou la validation ainsi que le fait de pouvoir tester toutes les classes.
Nous passons ces ensembles dans un ImageDataGenerator, dans lequel nous appliquons de la Data Augmentation pour les données de Train, et rescallons toutes les données entre [0, 1]
Les données alimentent ensuite le CNN après avoir été resizées en (224, 224) pour l'entrée dans le réseau de neurones.
Il est à noter que certaines images sont en niveau de gris (255 nuances de gris) et non en RGB.
Cette opérations peut être réalisée en préprocessing par le générateur d'images.
De plus nous savons que pour l'insertion dans le réseau de neurones les images vont être redimensionnées.
L'entrainement des réseaux s'avère très lourd et coûteux en ressources et en temps.
Aussi afin de réduire un peu le temps d'entrainement les images ont toutes été modifiées 'une fois pour toute' afin de gagner du temps au chargement d'images sur des tailles que de toute façon nous savons que nous allons diminuer à chaque étape.
L'opération aurait pû être réalisées en python mais l'a été manuellement à partir de convert d'Image Magick
ls $rep_in | while read toto
do
convert -colorspace Gray $rep_in/$toto -resize 100000@ $rep_out/$toto
done
L'option -resize 100000@ appliquant un redimensionnement (conservant la résolution) tel que le nombre de pixels ne dépasse pas 100 000
Les images sont donc toutes transformées en niveau de gris
Les données ont été entrainées sur un serveur p2.xlarge chez AWS EC2.
Pour un nombre d'EPOCH allant jusqu'à 50 avec un nombre réduit d'images les temps de traitement ont été de 7h avec l'usage de GPU
La modélisation de classification n'a pas donné de résultats satisfaisant.
En effet l'accuracy relevée était très basse aux alentours de acc: 0.1562 et les valeurs d'accuracy de validation restait autour de val_acc: 2.1580e-04 sans évolution.
import json
file1 = './../AWS_JEUDI/GOOD/3_2_history_gpu_history.json'
file3 = './../AWS_JEUDI/output/3_9_bis_INCEPTION_V3_history_gpu_history.json'
with open(file1, 'r') as f: data1 = json.loads(f.read())
with open(file3, 'r') as f: data3 = json.loads(f.read())
def affiche_graph(data, titre=''):
def affiche_one(data, d_param):
plt.plot(data[d_param['x_key']])
plt.plot(data[d_param['y_key']])
plt.title('model {}'.format(d_param['type_model']))
plt.ylabel(d_param['type_model'])
plt.xlabel('epoch')
plt.legend(['train', 'test'], loc='upper left')
plt.figure(figsize=(15, 7), )
plt.subplot(121)
affiche_one(data, {'x_key': 'acc', 'y_key': 'val_acc', 'type_model': 'accuracy'})
plt.subplot(122)
affiche_one(data, {'x_key': 'loss', 'y_key': 'val_loss', 'type_model': 'loss'})
plt.suptitle(titre, fontsize=12)
plt.show()
affiche_graph(data1, titre="Performances avec 5005 classes\n"
" avec ajout des images augmentées\n"
"pour avoir au moins 5 images par classe")
affiche_graph(data3, 'Inception V3 avec 5005 classes')
Dans un but de tester le modèle sur des données qui permettent de montrer la performance dans certaines conditions (afin de vérifier qu'il n'y a pas de bug) un filtrage a été réalisé sur les données.
Seules les classes ayant plus de 20 images sont prises en compte : cela concerne 60 classes différentes.
A la suite de l'entrainement une accuracy de 0.83 a été obtenue pour l'entrainement comme la validation après 2 ECOCH. 🤗

file_test_9 = './../AWS_JEUDI/output/3_9_BIS_DF_TEST_PREDICTION.csv'
file_prediction_challenge_9 = './../AWS_JEUDI/output/3_9_BIS_DF_PREDICTION_CHALLENGE.csv'
file_test_11 = './../AWS_JEUDI/output/3_11_DF_TEST_PREDICTION.csv'
df_test_9 = pd.read_csv(file_test_9, index_col=[0])
df_challenge_9 = pd.read_csv(file_prediction_challenge_9, index_col=[0])
df_test_11 = pd.read_csv(file_test_11, index_col=[0])
df_test_9[['new_whale', 'w_0bc078c', 'Real_Class_Name', 'Real_Class_Id']].head()
def get_score_from_list(name_good_class, l_prediction):
if name_good_class in l_prediction[:5]:
return 1/(l_prediction.index(name_good_class)+1)
else:
return 0
def get_mesure(df_test, threshold):
'''
Détermine la moyenne des MAP@5
INPUT:
- df_test : le DataSet prédit à partir des données de test
- threshold : seuil obtenu par 'new_whale' à partir duquel on met 'new_whale' en 1e position de la liste.
'''
l_col = list(df_test.columns)[:-2]
id_2_name = dict()
for nb, name in enumerate(l_col):
id_2_name[nb] = name
matrice = df_test.values[:, :-2]
resultat_indices_meilleurs = np.argsort(matrice, axis=1)[::-1][:,:5]
resultat_valeurs_meilleurs = np.sort(matrice, axis=1)[::-1][:,:5]
l_res = []
for i, val_new_whale in zip(resultat_indices_meilleurs, df_test['new_whale'].values):
tmp1 = [id_2_name[x] for x in i]
if (val_new_whale>threshold):
tmp1 = ['new_whale'] + tmp1
l_res.append(tmp1)
y = list(df_test['Real_Class_Name'].values)
l_mesure = []
for good, predict in zip(y, l_res):
l_mesure.append(get_score_from_list(good, predict))
return np.mean(l_mesure)
Les mesures pour des threshold de 0.0, 0.9 et 1.0
get_mesure(df_test_9, 0.0), get_mesure(df_test_9, 0.9), get_mesure(df_test_9, 1.0),
%%time
m_res = []
m_x = np.linspace(0.4, 1.0, num=40)
for i in m_x:
m_res.append(get_mesure(df_test_9,i))
plt.scatter(x=m_x, y=np.array(m_res))
plt.xlabel('Threshold pour "new_whale"')
plt.ylabel('Score du Challenge')
plt.show()
Dans ce cas de modélisation le seuil doit être fixé à 0.0 pour obtenir la meilleure note de 0.83.
Cela signifie donc que par rapport à la prédiction des données seule compte le fait de forcer la place de 'new_whale' en 1e place de la liste des proposition de toutes les lignes.
En regardant les données utilisées pour cet entrainement nous voyons
df_reparition_classes = df_train.Id.value_counts()
df_nb_plus_de_20_images = df_reparition_classes[df_reparition_classes>20]
pd.DataFrame({"Nombre d'images":df_nb_plus_de_20_images})[:5]
l_df_image_ok = []
for image_name in df_nb_plus_de_20_images.index:
l_df_image_ok.append(df_train[df_train.Id==image_name])
df_classes_avec_beaucoup_images = pd.concat(l_df_image_ok)
df_classes_avec_beaucoup_images.head()
t6 = df_classes_avec_beaucoup_images.Id.value_counts()
fig, ax = plt.subplots(figsize=(7, 7), subplot_kw=dict(aspect="equal"))
def func(pct, allvals):
if pct>10:
return "{:.2f} %".format(pct)
else:
return None
wedges, texts, autotexts = ax.pie(t6, autopct=lambda pct: func(pct, t6),
textprops=dict(color="w"))
ax.legend(wedges, t6[t6.values>55].index,
title="Id",
loc="center left",
bbox_to_anchor=(1, 0, 2.0, 1.5))
plt.setp(autotexts, size=20, weight="bold")
ax.set_title("Répartition du nombre\nd'Images par classe", fontsize=13)
plt.show()
En fait les 83 % d'accuracy semblent très liés à la proportion des image de classe 'new_whale' par rapport aux autres images dans les données en entrées de la modélisation
def get_confusion_matrix(df_test):
'''
Détermine le label obtenant la meilleure probabilité de prédiction
et renvoie la matrice de confusion
'''
from sklearn.metrics import confusion_matrix
l_col = list(df_test.columns)[:-2]
id_2_name = dict()
for nb, name in enumerate(l_col):
id_2_name[nb] = name
matrice = df_test.values[:, :-2]
resultat_indices_meilleurs = np.argsort(matrice, axis=1)[::-1][:,0]
y_predict = np.array([id_2_name[i] for i in resultat_indices_meilleurs])
y_true = df_test.Real_Class_Name.values
cm = pd.DataFrame(confusion_matrix(y_true, y_predict))
cm.columns = df_test.columns[:-2]
cm.index = df_test.columns[:-2]
return cm
cm_from_test_9 = get_confusion_matrix(df_test_9)
cm_from_test_9.head()
# Les classes les plus présentes dans les données prédites
np.sum(cm_from_test_9, axis=0).sort_values(ascending=False).head(10)
# Les classes les plus présentes dans les données de test sont
np.sum(cm_from_test_9, axis=1).sort_values(ascending=False).head(10)
En fait aucune 'new_whale' n'a été prédite
La modélisation de classification par Transfer Learning n'a ici rien donné du fait des faibles scores obtenus.
Cette étape étant nécessaire (par rapport à la stratégie proposée) pour l'élaboration d'une liste de prédiction, la suite de l'analyse n'a malheureusement pas pu aller plus loin.
Par la suite il faut identifier pour quelles raisons celle-ci n'a pas donné de résultat:
Ces doutes étant apparus tardivement (après plusieurs essais longs de modélisation) il ne semblait plus possible d'envisager cet entrainement supplémentaire.